#
# Copyright (c) 2023 supercell
#
# SPDX-License-Identifier: BSD-3-Clause
#
require "./text_parser"

module Luce
  class LinkParser < TextParser
    # If there is a valid link formed.
    getter? valid : Bool = false

    # Link label
    getter label : String? = nil

    # Link destination
    getter destination : String? = nil

    # Link title
    getter title : String? = nil

    def initialize(@source : String)
    end

    # How many lines of the `source` have been consumed by link reference
    # definition.
    getter unconsumed_lines : Int32 = 0

    # Parses `source` to a link reference definition
    def parse_definitions : Nil
      return if !parse_label() || done?() || char_at() != Charcode::COLON

      # Advance to the next character after the colon.
      advance()
      return unless parse_destination

      preceding_whitespaces = move_through_whitespace()
      if done?
        @valid = true
        return
      end

      multiline = char_at() == Charcode::LF
      preceding_whitespaces += move_through_whitespace(multiline: true)

      # The title must be preceded by whitespaces
      if preceding_whitespaces == 0 || done?
        @valid = done?
        return
      end

      has_valid_title = parse_title()
      # For example: `[foo]: <bar> "baz` is an invalid definition, but this one
      # is valid:
      #
      # ```
      # [foo]: <bar>
      # "baz
      # ``
      return if !has_valid_title && !multiline

      if has_valid_title
        move_through_whitespace()
        if !done?() && char_at() != Charcode::LF
          # It is not a valid definition if the title is followed by
          # non-whitespace characters, for example: `[foo]: <bar> "baz" hello`.
          # See https://spec.commonmark.org/0.30/#example-209.
          return unless multiline
          # But it is a valid link reference definition if this definition is
          # multiline, see https://spec.commonmark.org/0.30/#example-210.
          @title = nil
        end
      end

      lines_unconsumed = source[pos..].split("\n")
      if !lines_unconsumed.empty? && lines_unconsumed.first.blank?
        lines_unconsumed.delete_at(0)
      end

      @unconsumed_lines = lines_unconsumed.size

      @valid = true
    end

    # Parses the link label, returns `true` if there is a valid link label.
    def parse_label : Bool
      move_through_whitespace(multiline: true)

      return false if size - pos < 2

      return false if char_at != Charcode::LBRACKET

      # Advance past the opening `[`.
      advance()
      start = pos

      # A link label can have at most 999 characters inside the square brackets.
      # See https://spec.commonmark.org/0.30/#link-label.
      max_loop = 999
      loop do
        return false if (max_loop -= 1) < 0
        char = char_at(pos)
        case char
        when Charcode::BACKSLASH
          advance()
        when Charcode::LBRACKET
          return false
        when Charcode::RBRACKET
          break
        end
        advance()
        return false if done?
      end

      text = self[start...pos]
      return false if text.blank?

      # Advance past the closing `]`.
      advance()
      @label = text
      true
    end

    # Parses the link destination, returns `true` there is a valid link
    # destination.
    private def parse_destination : Bool
      move_through_whitespace(multiline: true)
      return false if done?

      is_valid_destination = if char_at() == Charcode::LT
                               parse_bracketed_destination()
                             else
                               parse_bare_destination()
                             end

      is_valid_destination
    end

    # Parses bracketed destinations (destinations wrapped in `<...>`). The
    # current position of the parser must be the first character of the
    # destination.
    #
    # Returns `true` if there is a valid link destination.
    private def parse_bracketed_destination : Bool
      # Walk past the opening `<`.
      advance

      start = pos
      loop do
        char = char_at()
        case char
        when Charcode::BACKSLASH
          advance()
        when Charcode::LF, Charcode::CR, Charcode::FF
          return false
        when Charcode::GT
          break
        end
        advance()
        return false if done?
      end

      @destination = self[start...pos]

      # Advance past the closing `>`.
      advance()
      true
    end

    # Parse "bare" destinations (destinations _not_ wrapped in `<...>`). The
    # current position of the parser must be the first character of the
    # destination.
    #
    # Returns `true` if there is a valid link destination.
    private def parse_bare_destination : Bool
      paren_count = 0
      start = pos

      loop do
        char = char_at
        case char
        when Charcode::BACKSLASH
          advance()
        when Charcode::SPACE, Charcode::LF, Charcode::CR, Charcode::FF
          break
        when Charcode::LPAREN
          paren_count += 1
        when Charcode::RPAREN
          paren_count -= 1
          if paren_count == 0
            advance()
            break
          end
        end

        advance()

        # There is no ending delimiter, so `done?` also means it is at the
        # end of a link destination.
        break if done?
      end

      @destination = self[start...pos]
      true
    end

    # Parses the **optional** link title, returns `true` if there is a valid
    # link title.
    private def parse_title : Bool
      # See: https://spec.commonmark.org/0.30/#link-title
      # The whitespace should be followed by a title delimiter.
      delimiter = char_at()
      return false if delimiter != Charcode::APOSTROPHE && delimiter != Charcode::QUOTE && delimiter != Charcode::LPAREN

      close_delimiter = delimiter == Charcode::LPAREN ? Charcode::RPAREN : delimiter
      advance()
      return false if done?
      start = pos

      loop do
        char = char_at()
        advance() if char == Charcode::BACKSLASH
        break if char == close_delimiter
        advance()
        return false if done?
      end

      return false if done?

      @title = self[start...pos]

      # Advance past the closing delimiter
      advance()
      true
    end
  end
end
